package czopyk.model.structure;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import model.Event;
import model.Time;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import czopyk.model.cost.CostMapper;
import czopyk.model.cost.InterferenceManager;
import czopyk.model.factories.Locator;
import czopyk.model.request.Request;

public class Center {

	public final Logger log;

	protected final Observable o = new Observable();

	protected CenterClass centerClass;

	// kolejka
	private final LinkedList<Request> queue = new LinkedList<Request>();
	// kolejka secondary
	private final Map<Request, Iterator<Request>> queueBackup = new HashMap<Request, Iterator<Request>>();

	// czasy rozpoczecia obslugi - lokalny pomiar czasu
	private final Map<Request, Double> serviceBegining;
	// referencje na zdarzenia konca
	private Map<Request, RequestFinishEvent> processedRequestsFinishEvent = new HashMap<Request, RequestFinishEvent>();
	protected final List<Center> next = new ArrayList<Center>();
	// ile podzgloszen jest aktualnie w kolejce
	protected Map<Request, Integer> currentRequests = new HashMap<Request, Integer>();
	private int channels;
	private final String name;
	private final AtomicInteger i = new AtomicInteger(0);

	protected CostMapper costMapper = Locator.getCostMapper();
	// referencja na caly symulator w celu umozliwienia
	// mechanizmu barier
	protected Simulator simulator;

	protected InterferenceManager interferenceManager = Locator
			.getInterferenceManager();

	private CenterLoadCounter loadCounter;

	public Center(String name, int channels, CenterClass cc) {
		this.centerClass = cc;
		this.channels = channels;
		this.name = name != null ? name : "center";
		serviceBegining = new HashMap<Request, Double>(channels);
		log = Logger.getLogger(this.name);
		loadCounter = new CenterLoadCounter(this.centerClass.getValue());
	}

	public void queue(final Request r) {
		if (log.isDebugEnabled()) {
			log.debug("queueing request " + r.getId());
		}

		// request r jest calkowity
		if (blockRequest(r)) {
			log.debug("request blocked");
			return;
		}

		if (r.getParent() != r) {
			throw new IllegalStateException(r + " queue in center " + this
					+ " is not integral!");
		}

		Set<Request> ers = Locator.getCostMapper().getSupportedRequests(r,
				centerClass);
		if (log.isDebugEnabled()) {
			log.debug("request '" + r.getId() + "' queued; supported requests: "
					+ ers.size());
		}

		if (ers.isEmpty()) {
			requestFinishInternal(r);
			// przeslij na wyjscie
		} else {
			Iterator<Request> iterator = ers.iterator();
			Request firstRequest = iterator.next();
			iterator.remove();
			queueInternal(firstRequest, true);
			if (!ers.isEmpty()) {
				queueBackup.put(r, iterator);
			}
			/*
			 * for (Request er : ers) { queueInternal(er, true); }
			 */
		}

		pool();
	};

	private boolean canServeNow() {
		return !queue.isEmpty() && processedRequestsFinishEvent.size() < channels;
	}

	Set<Request> currentRequestsParents = new HashSet<Request>();

	private ListIterator<Request> getRequestFromQueueToBeServed() {
		// log.debug("queuing next request: avoid sibling queuing...");
		currentRequestsParents.clear();
		for (Request currentRequest : processedRequestsFinishEvent.keySet()) {
			currentRequestsParents.add(currentRequest.getParent());
			// log.debug("current requests parent: " +
			// currentRequest.getParent().getId());
		}

		ListIterator<Request> iter = queue.listIterator();

		Request potencialRequestParent = null;
		Request potencialRequest = null;
		boolean flag;
		while (iter.hasNext()) {
			flag = true;
			potencialRequest = iter.next();
			potencialRequestParent = potencialRequest.getParent();
			for (Request currentRequestParent : currentRequestsParents) {
				if (potencialRequestParent == currentRequestParent) {
					// log.debug("request " + potencialRequest.getId() + " with parent " +
					// potencialRequestParent.getId() + " denied");
					flag = false;
					break;
				}
			}

			if (flag) {
				// log.debug("request " + potencialRequest.getId() + " with parent " +
				// potencialRequestParent.getId() + " accepted");
				iter.previous();
				return iter;
			}
		}
		return null;
	}

	protected void pool() {
		// load
		updateLoad();

		ListIterator<Request> iter = null;
		if (canServeNow() && (iter = getRequestFromQueueToBeServed()) != null) {

			Request r = iter.next();
			iter.remove();

			RequestFinishEvent e = makeRequestFinishEventForRequest(r,
					(this instanceof Cache) ? ((Cache) this).skip() : false);

			processedRequestsFinishEvent.put(r, e);

			Time.registerEvent(e);
		}
		fixCurrentServiceTime();
	}

	private RequestFinishEvent makeRequestFinishEventForRequest(Request r,
			boolean skip) {
		double factor = interferenceManager.getInterferenceFactor(centerClass, r,
				processedRequestsFinishEvent.size() + 1);
		return new RequestFinishEvent(r, factor, Time.currentTime()
				+ (factor * costMapper.getCost(r.getLc(), centerClass)), skip);
	}

	private void fixCurrentServiceTime() {
		for (Request r : processedRequestsFinishEvent.keySet()) {
			final double factor = interferenceManager.getInterferenceFactor(
					centerClass, r, processedRequestsFinishEvent.size());

			RequestFinishEvent oldEvent = processedRequestsFinishEvent.get(r);

			if (oldEvent.getCurrentFactor() != factor
					&& oldEvent.getTime() != Time.currentTime()) {
				final double timeLeft = (oldEvent.getTime() - Time.currentTime())
						* (factor / oldEvent.getCurrentFactor());

				if (log.isDebugEnabled()) {
					log.debug("correcting time for event " + oldEvent + ": "
							+ (oldEvent.getTime() - Time.currentTime()) + " -> " + timeLeft);
				}

				Time.cancelEvent(oldEvent);

				RequestFinishEvent e = new RequestFinishEvent(r, factor,
						Time.currentTime() + timeLeft, oldEvent.skip);

				processedRequestsFinishEvent.put(r, e);

				Time.registerEvent(e);
			}
		}
	}

	private class RequestFinishEvent extends Event {

		private final Request r;
		private final double currentFactor;
		private final boolean skip;

		public RequestFinishEvent(final Request r, double currentFactor,
				double timeLeft, boolean skip) {
			super(timeLeft);
			this.r = r;
			this.currentFactor = currentFactor;
			this.skip = skip;
		}

		public void perform() {
			final double ct = Time.currentTime();
			Double serviceTime = ct - serviceBegining.remove(r);
			if (!skip) {
				r.done(name);
			}

			// koniecznie przed requestFinishInternal
			Iterator<Request> iter = queueBackup.get(r.getParent());
			if (iter != null) {
				if (!iter.hasNext()) {
					queueBackup.remove(iter);
				} else {
					Request next = iter.next();
					iter.remove();
					queueInternal(next, true);
				}
			}

			Center.this.requestFinishInternal(r);
			if (log.isDebugEnabled()) {
				log.debug("finished " + r + " at " + ct + " (took " + serviceTime + ")");
			}
			/*
			if (name.equals("cache")) {
				log.info("finished " + r + " at " + ct + " (took " + serviceTime + ")");
			}
			*/
			updateLoad();
			processedRequestsFinishEvent.remove(r);
			pool();
		}

		public double getCurrentFactor() {
			return currentFactor;
		}

		@Override
		public String getName() {
			return "RequestFinishEvent";
		}
	}

	public List<Center> getAllNext() {
		return next;
	}

	public int getChannels() {
		return channels;
	}

	public Center setChannels(int channels) {
		if (channels <= 0) {
			throw new IllegalArgumentException("Cannot have " + channels
					+ " channel(s).");
		}
		this.channels = channels;
		return this;
	}

	protected void requestFinishInternal(Request r) {
		r = r.getParent();
		// ile podzgloszen jest aktualnie w kolejce
		Integer i = currentRequests.get(r);
		if (i == null || i == 1) {
			// i == null wtw gdy nie bylo obslugiwanych żądań

			currentRequests.remove(r);

			// zdjecie indexu
			simulator.unlockIndex(r, this);

			// to bylo ostatnie - powiadomienie o zakonczeniu przetwarzania
			o.notifyObservers(r);

		} else {
			currentRequests.put(r, i - 1);
		}
	}

	public CenterClass getCenterClass() {
		return centerClass;
	}

	public Center getNext() {
		// round robin
		Center c = next.get(i.getAndIncrement());
		i.compareAndSet(next.size(), 0);
		return c;
	}

	// odrzucenie przyjecia requestu do kolejki
	// symulacja blokady po powrotach z asynchronicznych obliczen
	protected boolean blockRequest(Request r) {
		return simulator.blockRequest(r, this);
	}

	/**
	 * Queues single request
	 * */
	protected void queueInternal(final Request r, final boolean dontSkip) {
		final Request parent = r.getParent();
		Integer i = currentRequests.get(parent);
		currentRequests.put(parent, (i == null) ? 1 : i + 1);

		queue.add(r);
		serviceBegining.put(r, Time.currentTime());
	}

	@Override
	public String toString() {
		return toStringOffset(0);
	}

	public String toStringOffset(int offset) {
		StringBuilder sb = new StringBuilder("Center " + name + " [channels="
				+ getChannels() + ", centerClass=" + centerClass + "]");
		for (Center c : getAllNext()) {
			sb.append("\n" + StringUtils.repeat("  ", offset) + "-> "
					+ c.toStringOffset(offset + 2));
		}
		return sb.toString();
	}

	public String getName() {
		return name;
	}

	public void setSimulator(Simulator simulator) {
		this.simulator = simulator;
	}

	public double getUtilizationAndReset() {
		updateLoad();
		double load = loadCounter.getLoadAndReset();
		if (log.isDebugEnabled()) {
			log.debug("getting utilization of " + this + " -> " + load);
		}
		return load;
	}

	private void updateLoad() {
		loadCounter.registerLoadSinceLastTime(processedRequestsFinishEvent.size());
	}

}
